using System;
using System.Text;
using System.Threading;
using CodeConsult.Nordic.nRF8001;
using CodeConsult.Nordic.nRF8001.ApplicationControlInterface.Commands.DataCommands;
using CodeConsult.Nordic.nRF8001.ApplicationControlInterface.Commands.SystemCommands;
using CodeConsult.Nordic.nRF8001.ApplicationControlInterface.Events;
using CodeConsult.Nordic.nRF8001.ApplicationControlInterface.Events.CommandResponseEvents;
using CodeConsult.Nordic.nRF8001.ApplicationControlInterface.Events.DataEvents;
using CodeConsult.Nordic.nRF8001.ApplicationControlInterface.Events.SystemEvents;
using Microsoft.SPOT;

namespace CodeConsult.Adafruit.BLE_UART
{
    public class NordicRf8001
    {
        private const int MaxChunkSize = 20;
        private readonly byte[] _maxChunk = new byte[MaxChunkSize];

        private readonly Aci _aci;
        private readonly AciConfig _aciConfig;
        private int _setupCommandsRespondsReceived = 0;
        private int _setupCommandsRespondsExpected = 0;
        private bool _changeTimingRequestSent = false;

        private readonly PipesManager _pipesManager = new PipesManager();
        private  DataCreditManager _dataCreditManager;
        private readonly AutoResetEvent _setupCompletedResetEvent = new AutoResetEvent(false);
        private readonly AutoResetEvent _setupCommandRespondResetEvent = new AutoResetEvent(false);

        // FUTURE: lock per Tx pipe
        private readonly object _writeLock = new object();

        public delegate void DataReceivedEventHandler(byte[] data);
        public event DataReceivedEventHandler OnDataReceived = delegate { };

        public ushort AdvertisingTimeout { get; set; } = 0;
        public ushort AdvertisingInterval { get; set; } = 80;

        public NordicRf8001(NordicRf8001Config bleUartConfig)
        {
            _aci = new Aci();
            _aciConfig = new AciConfig
            {
                SpiModule = bleUartConfig.SpiModule,
                ReqInputPin = bleUartConfig.ReqInputPin,
                ActOutputPin = bleUartConfig.ActOutputPin,
                RdyOutputPin = bleUartConfig.RdyOutputPin,
                RstInputPin = bleUartConfig.RstInputPin,
                McuSpiBitOrder = bleUartConfig.McuSpiBitOrder == BitOrder.Lsb ? Nordic.nRF8001.BitOrder.Lsb : Nordic.nRF8001.BitOrder.Msb
            };
            _aci.OnEventReceived += HandleOnEventReceived;
        }

        public void WaitForSetupDone()
        {
            _aci.Initialize(_aciConfig);
            _setupCompletedResetEvent.WaitOne(-1, false);
            Debug.Print("nRF8001 settting up done");
        }

        public void Write(byte[] buffer, byte pipe = RfGoStudioServices.UartOverBtleUartTxTxPipe)
        {
            var isTxPipe = _pipesManager.IsTxPipe(pipe);

            if (!isTxPipe || !_pipesManager.IsPipeAvailable(pipe)) return;

            lock (_writeLock)
            {
                var bytesSent = 0;
                while (bytesSent < buffer.Length)
                {
                    _dataCreditManager.WaitForCreditAvailable();

                    var chunkSize = System.Math.Min(buffer.Length - bytesSent, MaxChunkSize);
                    var chunk = chunkSize < MaxChunkSize ? new byte[chunkSize] : _maxChunk;
                    Array.Copy(buffer, bytesSent, chunk, 0, chunkSize);

                    _dataCreditManager.UpdateCreditAvailable(-1);

                    _aci.EnqueueCommand(new SendDataCommand
                    {
                        ServicePipeNumber = pipe,
                        Data = chunk
                    });

                    bytesSent += chunkSize;

                    Thread.Sleep(35);
                }
            }
        }

        private void ExecuteSetup()
        {
            new Thread(() =>
            {
                Debug.Print("nRF8001 setting up...");
                _setupCommandsRespondsExpected = RfGoStudioServices.SetupCommandMessages.Length;
                foreach (var setupCommandMessage in RfGoStudioServices.SetupCommandMessages)
                {
                    _aci.EnqueueMessage(setupCommandMessage, true);
                    _setupCommandRespondResetEvent.WaitOne(-1, false);
                }
            }).Start();
        }

        private void HandleOnEventReceived(AciEventBase receivedEvent)
        {
            if (receivedEvent == null || receivedEvent.Error) return;

            switch (receivedEvent.DecodedOpCode)
            {
                case StartedEvent.OpCode:
                    Debug.Print("nRF8001 started");
                    var deviceStartedEvent = (StartedEvent)receivedEvent;
                    _dataCreditManager = new DataCreditManager(deviceStartedEvent.CreditAvailable);
                    switch ((NordicRf8001AciEnums.OperatingMode)deviceStartedEvent.OperatingMode)
                    {
                        case NordicRf8001AciEnums.OperatingMode.Setup:
                            ExecuteSetup();
                            break;
                        case NordicRf8001AciEnums.OperatingMode.Standby:
                            Debug.Print("nRF8001 standby");
                            _aci.EnqueueCommand(new SetLocalDataCommand
                            {
                                ServicePipeNumber = RfGoStudioServices.GapDeviceNameSetPipe,
                                Data = Encoding.UTF8.GetBytes("NETMFnRF8001")
                            });

                            _aci.EnqueueCommand(new ConnectCommand
                            {
                                Timeout = AdvertisingTimeout,
                                AdvInterval = AdvertisingInterval
                            });
                            break;
                    }
                    break;

                case CommandResponseEvent.OpCode:
                    if (_setupCommandsRespondsReceived < _setupCommandsRespondsExpected)
                    {
                        _setupCommandsRespondsReceived++;
                        _setupCommandRespondResetEvent.Set();
                        if (_setupCommandsRespondsReceived == _setupCommandsRespondsExpected)
                        {
                            _setupCompletedResetEvent.Set();
                        }

                        return;
                    }

                    var commandResponseEvent = (CommandResponseEvent)receivedEvent;
                    if (commandResponseEvent.CommandOpCode == GetDeviceVersionCommand.OpCode)
                    {
                        if (_pipesManager.HasLocalStoreLocatiom(RfGoStudioServices.DeviceInformationHardwareRevisionStringSetPipe))
                        {
                            var decodeHelper = new DecodeHelper { Buffer = commandResponseEvent.ResponseData };
                            var configurationId = decodeHelper.GetUnsignedShort(DecodeHelper.ByteOrder.LsbMsb);
                            var aciProtocolVersion = decodeHelper.GetByte();
                            var currentSetupFormat = decodeHelper.GetByte();
                            var setupId = decodeHelper.GetUnsignedInt(DecodeHelper.ByteOrder.LsbMsb);
                            var configurationStatus = decodeHelper.GetByte();
                            var hardwareRevisionString = configurationId + "." + aciProtocolVersion + "." + currentSetupFormat + "." + setupId + "." + configurationStatus;
                            var hardwareRevisiobStringBytes = Encoding.UTF8.GetBytes(hardwareRevisionString.Substring(0, System.Math.Min(hardwareRevisionString.Length, 20)));

                            _aci.EnqueueCommand(new SetLocalDataCommand
                            {
                                ServicePipeNumber = RfGoStudioServices.DeviceInformationHardwareRevisionStringSetPipe,
                                Data = commandResponseEvent.ResponseData
                            });
                        }
                    }
                    break;

                case ConnectedEvent.OpCode:
                    _dataCreditManager.ResetCreditAvailable(); 
                    _aci.EnqueueCommand(new GetDeviceVersionCommand());
                    break;

                case DisconnectedEvent.OpCode:
                    _dataCreditManager.SetCreditAvailable(0);
                    _pipesManager.ResetPipesAvailable();
                    _aci.EnqueueCommand(new ConnectCommand
                    {
                        Timeout = AdvertisingTimeout,
                        AdvInterval = AdvertisingInterval
                    });
                    break;

                case DataCreditEvent.OpCode:
                    var dataCreditEvent = (DataCreditEvent)receivedEvent;
                    _dataCreditManager.UpdateCreditAvailable(dataCreditEvent.DataCredits);
                    break;

                case PipeStatusEvent.OpCode:
                    var pipeStatusEvent = (PipeStatusEvent)receivedEvent;
                    _pipesManager.UpdatePipesAvailable(pipeStatusEvent.PipesOpen, pipeStatusEvent.PipesClosed);
                    if (!_changeTimingRequestSent)
                    {
                        _aci.EnqueueCommand(new ChangeTimingRequestNoTimingParametersCommand());
                        _changeTimingRequestSent = true;
                    }

                    break;

                case PipeErrorEvent.OpCode:
                    var pipeErrorEvent = (PipeErrorEvent)receivedEvent;
                    if (!_pipesManager.IsTxPipe(pipeErrorEvent.ServicePipeNo) || pipeErrorEvent.ErrorCode != (byte)PipesManager.PipeErrorCode.AciStatusErrorCreditNotAvailable)
                        _dataCreditManager.UpdateCreditAvailable(1);
                    break;

                case DataReceivedEvent.OpCode:
                    var dataReveivedEvent = (DataReceivedEvent)receivedEvent;
                    OnDataReceived?.Invoke(dataReveivedEvent.Data);
                    break;
            }
        }
    }
}
